package com.symaster.common.geom;

import cn.hutool.core.util.ObjectUtil;
import com.symaster.common.geom.entity.Vector;
import com.symaster.common.geom.entity.*;
import com.symaster.common.geom.util.PointUtil;

import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author yinmiao
 * @version 2022-08-17 14:57
 */
public class ShapeSpace<T extends Node> implements ViewPolygonPanel {

    private final double allScale;
    private static final double MAXIMUM_ALLOWABLE_ERROR_VALUE = 0.0000001D;
    private final double srcWidth;
    private final double srcHeight;
    private final double width;
    private final double height;
    private double maxY = 0.0D;
    private double maxX = 0.0D;
    private final Map<Integer, T> srcNodeMapById = new HashMap<>();
    private final List<T> srcNodes = new LinkedList<>();
    private final Map<Integer, T> nodeMapById = new HashMap<>();
    private final List<T> nodes = new LinkedList<>();

    public ShapeSpace(double width, double height, double allScale) {
        this.allScale = allScale;
        this.srcWidth = width;
        this.srcHeight = height;
        this.width = srcWidth * this.allScale;
        this.height = srcHeight * this.allScale;
    }

    public ShapeSpace(double width, double height) {
        this(width, height, 0.07);
    }

    public void add(T node) {
        check(node);

        addToIndexMap(node); // 添加索引映射
        initMovePoint(node.getId()); // 初始化图形坐标
        autoLayout(node.getId()); // 自动对齐
        update();

    }

    public void add(T node, boolean resetCoordinates) {
        addToIndexMap(node); // 添加索引映射
        if (resetCoordinates) {
            node.move(0, 0);
        }
        update();
    }

    public T remove(Integer id) {
        T remove1 = nodeMapById.remove(id);
        nodes.remove(remove1);
        T remove = srcNodeMapById.remove(id);
        srcNodes.remove(remove);
        update();
        remove.move(remove1.getX() / allScale, remove1.getY() / allScale);
        return remove;
    }

    public double getUseArea() {
        double area = 0.0D;
        for (T node : srcNodes) {
            area += node.getPolygon().getArea();
        }
        return area;
    }

    /**
     * 初始化图形坐标
     */
    public void initMovePoint(Integer id) {
        T node = myFindNode(id);

        if (Math.abs(maxY) < MAXIMUM_ALLOWABLE_ERROR_VALUE) {
            node.move(0, 0);
            return;
        }

        double dstX = width - node.getWidth() - 1;
        double dstY = maxY + node.getMarge();
        node.move(dstX, dstY);
        int count = 0;
        while (moveOver(id)) {
            dstY = dstY + node.getMarge();
            node.move(dstX, dstY);
            count++;
            if (count > 1000) {
                throw new IllegalArgumentException();
            }
        }
    }

    /**
     * 自动对齐
     */
    public void autoLayout(Integer id) {

        T node = myFindNode(id);
        if (nodes.size() <= 1) {
            node.move(0, 0);
            return;
        }

        // double offset = Math.max(node.getMarge() * 0.5, 0.5);
        // Point minPoint = new Point(node.getX(), node.getY());
        // while (node.getX() >= 0) {
        double appropriateY = getAppropriateY(node);
        moveY(node, appropriateY);
        //     minPoint = minPoint(new Point(node.getX(), node.getY()), minPoint);
        //     //     // while (node.getY() >= 0) {
        //     //     //     if (translateY(node, -offset)) minPoint = minPoint(new Point(node.getX(), node.getY()), minPoint);
        //     //     //     else break;
        //     //     // }
        //     moveX(node, node.getX() - offset);
        // }
        // node.move(minPoint);
    }

    public void cavityAutoLayout(Integer id) {
        T node = myFindNode(id);
        if (nodes.size() <= 1) {
            node.move(0, 0);
            return;
        }

        double offset = Math.max(node.getMarge() * 0.5, 0.5);
        Point minPoint = new Point(node.getX(), node.getY());
        while (node.getX() >= 0) {
            double appropriateY = getCavityAppropriateY(node);
            moveY(node, appropriateY);
            minPoint = minPoint(new Point(node.getX(), node.getY()), minPoint);
            while (node.getY() > 0) {
                if (translateY(node, -offset)) {
                    minPoint = minPoint(new Point(node.getX(), node.getY()), minPoint);
                }
            }
            moveX(node, node.getX() - offset);
        }
        node.move(minPoint);
    }

    /**
     * 判断图形是否重叠以及是否移出画布
     *
     * @return true: 重叠或已经移出画布 | false: 未重叠并且未移出画布
     */
    public boolean moveOver(Integer id) {

        T node = myFindNode(id);

        /* 判断是否移出画布 */
        if (graphicsOutOfBounds(node)) {
            return true;
        }

        /* 测试是否与其他图形重叠 */
        double marge = node.getMarge();
        List<T> nodes = myFindNodes((node.getX() - marge), (node.getY() - marge), (node.getWidth() + (marge * 2)), (node.getHeight() + (marge * 2)));

        for (T nodeItem : nodes) {
            if (nodeItem.getId().equals(id)) {
                continue;
            }
            if (node.getSwellPolygon().collide(nodeItem.getPolygon())) {
                return true;
            }
        }

        return false;
    }

    public boolean moveOverNotMarge(Integer id) {

        T node = myFindNode(id);

        /* 判断是否移出画布 */
        if (graphicsOutOfBounds(node)) {
            return true;
        }

        /* 测试是否与其他图形重叠 */
        double marge = node.getMarge();
        List<T> nodes = myFindNodes((node.getX() - marge), (node.getY() - marge), (node.getWidth() + (marge * 2)), (node.getHeight() + (marge * 2)));

        for (T nodeItem : nodes) {
            if (nodeItem.getId().equals(id)) {
                continue;
            }
            if (node.getPolygon().collide(nodeItem.getPolygon())) {
                return true;
            }
        }

        return false;
    }

    public List<T> _getNodes() {
        return nodes;
    }

    public T _findNode(Integer id) {
        return nodeMapById.get(id);
    }

    public List<T> getNodes() {
        return srcNodes.stream().map(this::recall).collect(Collectors.toList());
    }

    @Override
    public Collection<Polygon> getPolygons() {
        return getNodes().stream().map(Node::getPolygon).collect(Collectors.toList());
    }

    @Override
    public int size() {
        return srcNodes.size();
    }

    @Override
    public double getWidth() {
        return srcWidth;
    }

    @Override
    public double getHeight() {
        return srcHeight;
    }

    public double getMaxY() {
        return maxY / allScale;
    }

    public double getMaxX() {
        return maxX / allScale;
    }

    public T findNode(Integer id) {
        return recall(id);
    }

    public List<T> findNodes(double x, double y, double width, double height) {
        List<T> ts = myFindNodes(x * allScale, y * allScale, width * allScale, height * allScale);
        return ts.stream().map(e -> recall(e.getId())).collect(Collectors.toList());
    }

    public double getAllScale() {
        return allScale;
    }

    void rotate(Integer id, double angle) {
        T t = myFindNode(id);
        if (t.getAngle() != null && t.getAngle() != 0.0D) {
            resetAngle(t);
        }
        double x = t.getX();
        double y = t.getY();
        t.rotateTransform(angle);
        t.move(x, y);
        t.setAngle(angle);
    }

    private void resetAngle(T t) {
        if (t.getAngle() == null || t.getAngle() == 0.0D) {
            return;
        }
        double x = t.getX();
        double y = t.getY();
        t.rotateTransform(-t.getAngle());
        t.move(x, y);
        t.setAngle(0.0D);
    }

    List<T> collideAll() {
        List<T> result = new ArrayList<>();
        for (T node : nodes) {
            if (moveOver(node.getId())) {
                result.add(node);
            }
        }
        return result;
    }

    public void move(Integer id, double x, double y) {
        T t = myFindNode(id);
        t.move(x, y);
    }

    double getCavityAppropriateY(T node) {

        List<T> nodes = myFindNodes((node.getX() - node.getMarge()), 0, (node.getWidth() + (node.getMarge() * 2)), height);
        double max = 0.0D;
        for (T node1 : nodes) {
            if (node1 == node) {
                continue;
            }
            max = Math.max(node1.getY() + node1.getHeight(), max);
        }
        return max + node.getMarge();
    }

    double getAppropriateY(T node) {

        double result = 0.0D;
        double x1 = node.getX() - node.getMarge();
        double y1 = 0.0D;
        double width = node.getWidth() + (node.getMarge() * 2);
        double height = node.getY() + node.getHeight();

        Rectangle2D.Double rect = new Rectangle2D.Double(x1, y1, width, height);

        List<T> nodes = myFindNodes(x1, y1, width, height);

        if (nodes.size() == 0) {
            return result;
        }

        LineSegment line = new LineSegment(new Point(x1, 0), new Point(x1, y1 + height + 100));

        List<Point> points2 = node.getPolygon().toPointList().stream()
                .filter(e -> rect.contains(e.x, e.y)).collect(Collectors.toList());

        Double minDis = null;
        for (T node1 : nodes) {
            if (node1 == node) {
                continue;
            }

            List<Point> points = node1.getPolygon().toPointList().stream()
                    .filter(e -> rect.contains(e.x, e.y)).collect(Collectors.toList());

            for (Point point : points) {
                // TODO 其他图形
                line.move(point.x, 0);
                List<Point> crossPoint2 = node.getPolygon().getCrossPoint(line);
                if (crossPoint2.size() == 0) {
                    continue;
                }
                Point point2 = yMinPoint(crossPoint2);
                if (point2.getY() < point.getY()) {
                    continue;
                }
                double dis = PointUtil.dis(point, point2);
                if (minDis == null || dis < minDis) {
                    minDis = dis;
                }
            }

            for (Point point : points2) {
                // TODO 自身所有点
                line.move(point.x, 0);
                List<Point> crossPoint = node1.getPolygon().getCrossPoint(line);
                if (crossPoint.size() == 0) {
                    continue;
                }
                Point point1 = yMaxPoint(crossPoint);
                if (point.getY() < point1.getY()) {
                    continue;
                }
                double dis = PointUtil.dis(point, point1);
                if (minDis == null || dis < minDis) {
                    minDis = dis;
                }
            }
        }

        if (minDis == null) {
            return 0.0D;
        }

        return Math.max(node.getY() - minDis + node.getMarge(), 0.0D);
    }

    private Point yMinPoint(List<Point> crossPoint2) {
        Point result = null;
        for (Point point : crossPoint2) {
            if (result == null || result.getY() > point.getY()) {
                result = point;
                continue;
            }
        }
        return result;
    }

    private Point yMaxPoint(List<Point> crossPoint) {
        Point result = null;
        for (Point point : crossPoint) {
            if (result == null || result.getY() < point.getY()) {
                result = point;
                continue;
            }
        }
        return result;
    }

    void check(T node) {
        if (node.getId() == null) {
            throw new IllegalArgumentException("Node.ID Is Null");
        }
        T t = srcNodeMapById.get(node.getId());
        if (t != null) {
            throw new IllegalArgumentException("Node.ID Must be unique");
        }
    }

    /**
     * 通过id查询node
     */
    T myFindNode(Integer id) {
        return nodeMapById.get(id);
    }

    /**
     * 通过矩形区域查询nodes
     */
    List<T> myFindNodes(double x, double y, double width, double height) {
        return this.nodes.stream().filter(e -> {
            if (e.getX() > x + width) {
                return false;
            }
            if (e.getY() > y + height) {
                return false;
            }
            if (e.getX() + e.getWidth() < x) {
                return false;
            }
            return !(e.getY() + e.getHeight() < y);
        }).collect(Collectors.toList());
    }

    T recall(T node) {
        T clone = ObjectUtil.clone(node);
        T t = nodeMapById.get(clone.getId());

        double angle_1 = t.getAngle() == null ? 0.0D : t.getAngle();
        double angle_2 = clone.getAngle() == null ? 0.0D : clone.getAngle();
        if (angle_1 != angle_2) {
            resetAngle(clone);
            clone.setAngle(angle_1);
            clone.rotateTransform(angle_1);
        }

        clone.move(t.getX() / allScale, t.getY() / allScale);
        return clone;
    }

    T recall(Integer id) {
        T node = srcNodeMapById.get(id);
        if (node == null) {
            return null;
        }
        return recall(node);
    }

    void update() {
        if (nodes.size() == 0) {
            return;
        }

        double maxY = 0.0D;
        double maxX = 0.0D;

        for (T node : nodes) {
            maxY = Math.max(node.getY() + node.getHeight(), maxY);
            maxX = Math.max(node.getX() + node.getWidth(), maxX);
        }

        this.maxY = maxY;
        this.maxX = maxX;
    }

    Point minPoint(Point p1, Point p2) {
        if (p1.getY() < p2.getY()) {
            return p1;
        }

        if (Math.abs(p1.getY() - p2.getY()) <= 0.001 && p1.getX() < p2.getX()) {
            return p1;
        }
        return p2;
    }

    int translateToUpper(T node) {
        int count = 0;
        while (translateY(node, -1)) {
            count++;
        }
        return count;
    }

    double translateToRight(T node) {
        int count = 0;
        while (translateX(node, 1)) {
            count++;
        }
        return count;
    }

    double translateToLeft(T node) {
        int count = 0;
        while (translateX(node, -1)) {
            count++;
        }
        return count;
    }

    boolean translateX(Integer id, double xn) {
        myFindNode(id).translate(new Vector(xn, 0));
        return !moveOver(id);
    }

    boolean translateY(Integer id, double yn) {
        myFindNode(id).translate(new Vector(0, yn));
        return !moveOver(id);
    }

    boolean translateX(T node, double xn) {
        node.translate(new Vector(xn, 0));
        return !moveOver(node.getId());
    }

    boolean translateY(T node, double yn) {
        node.translate(new Vector(0, yn));
        return !moveOver(node.getId());
    }

    void moveX(Integer id, double x) {
        T node = myFindNode(id);
        double v = x - node.getX();
        node.translate(v, 0);
    }

    void moveX(T node, double x) {
        double v = x - node.getX();
        node.translate(v, 0);
    }

    void moveY(Integer id, double y) {
        T node = myFindNode(id);
        double v = y - node.getY();
        node.translate(0, v);
    }

    void moveY(T node, double y) {
        double v = y - node.getY();
        node.translate(0, v);
    }

    /**
     * 测试图形是否越界
     */
    boolean graphicsOutOfBounds(T node) {
        if (node.getX() < 0 || node.getY() < 0) {
            return true;
        }
        return node.getX() + node.getWidth() > width;
    }

    /**
     * 添加索引映射
     */
    void addToIndexMap(T node) {
        srcNodes.add(node);
        srcNodeMapById.put(node.getId(), node);
        T clone = scale(node);
        nodes.add(clone);
        nodeMapById.put(clone.getId(), clone);
    }

    T scale(T node) {
        T clone = ObjectUtil.clone(node);
        clone.getPolygon().scaleTransform(new Point(0, 0), allScale);
        clone.setMarge(clone.getMarge() * allScale);
        clone.setPolygon(PointUtil.approxPolyDp(clone.getPolygon(), 2, false));
        clone.updateSwellPolygon();
        return clone;
    }

    public boolean equalsDouble(double d1, double d2) {
        return Math.abs(d1 - d2) < MAXIMUM_ALLOWABLE_ERROR_VALUE;
    }
}
